﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections;

namespace Jy.ComicDownloader.Picture {
    public class ImageInfo {
        public static readonly int FORMAT_JPEG = 0;
        public static readonly int FORMAT_GIF = 1;
        public static readonly int FORMAT_PNG = 2;
        public static readonly int FORMAT_BMP = 3;
        public static readonly int FORMAT_PCX = 4;
        public static readonly int FORMAT_IFF = 5;
        public static readonly int FORMAT_RAS = 6;
	    public static readonly int FORMAT_PBM = 7;
	    public static readonly int FORMAT_PGM = 8;
	    public static readonly int FORMAT_PPM = 9;
        public static readonly int FORMAT_PSD = 10;

        private static readonly string[] FORMAT_NAMES ={"JPEG", "GIF", "PNG", "BMP", "PCX", 
		 "IFF", "RAS", "PBM", "PGM", "PPM","PSD"};

        /**
	     * The names of the MIME types for all supported file formats.
	     * The FORMAT_xyz int constants can be used as index values for
	     * this array.
	     */
	    private static readonly string[] MIME_TYPE_STRINGS =
		{"image/jpeg", "image/gif", "image/png", "image/bmp", "image/pcx", 
		 "image/iff", "image/ras", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap", 
		 "image/psd"};

        
	    private int width;
	    private int height;
	    private int bitsPerPixel;
	    private bool progressive;
	    private int format;
	    private FileStream fs;
	    private bool collectComments = true;
        private ArrayList comments = new ArrayList();
	    private bool determineNumberOfImages = true;
	    private int numberOfImages;
	    private int physicalHeightDpi;
	    private int physicalWidthDpi;


        #region Properties

        public FileStream ImageSource {
            get {
                return fs;
            }
            set {
                fs = value;
            }
        }

        public int Format {
            get { return format; }
        }

        public string FormatName {
            get {
                if (format >= 0 && format < FORMAT_NAMES.Length) {
                    return FORMAT_NAMES[format];
                } else {
                    return "?";
                }
            }
        }

        public int Height {
            get {
                return height;
            }
        }

        public int Width {
            get {
                return width;
            }
        }

        public int BitsPerPixel {
            get {
                return bitsPerPixel;
            }
        }

        public string[] Comments {
            get {
                return (string[])comments.ToArray(typeof(string));
            }
        }

        public string MimeType {
            get {
                if (format >= 0 && format < MIME_TYPE_STRINGS.Length) {
                    if (format == FORMAT_JPEG && progressive) {
                        return "image/pjpeg";
                    }
                    return MIME_TYPE_STRINGS[format];
                } else {
                    return null;
                }
            }
        }

        public int NumberOfImages {
            get {
                return numberOfImages;
            }
        }

        public int PhysicalHeightDpi {
            get {
                return physicalHeightDpi;
            }
            set {
                physicalHeightDpi = value;
            }
        }

        public float PhysicalHeightInch {
            get{
                int h = Height;
		        int ph = PhysicalHeightDpi;
		        if (h > 0 && ph > 0) {
			        return ((float)h) / ((float)ph);
		        } else {
			        return -1.0f;
		        }
            }
            
        }

        public int PhysicalWidthDpi {
            get {
                return physicalWidthDpi;
            }
            set {
                physicalWidthDpi = value;
            }
        }

        public float PhysicalWidthInch {
            get {
                int w = Width;
                int pw = PhysicalWidthDpi;
                if (w > 0 && pw > 0) {
                    return ((float)w) / ((float)pw);
                } else {
                    return -1.0f;
                }
            }
        }

        public bool IsProgressive {
            get {
                return progressive;
            }
        }

        public bool DetermineNumberOfImages {
            get {
                return determineNumberOfImages;
            }
            set {
                determineNumberOfImages = value;
            }
        }

        #endregion

        public ImageInfo() {
            
        }

        private void AddComment(String s) {
            if (comments == null) {
                comments = new ArrayList();
            }
            comments.Add(s);
        }


        /**
         * Call this method after you have provided an input stream or file
         * using {@link #setInput(InputStream)} or {@link #setInput(DataInput)}.
         * If true is returned, the file format was known and information
         * on the file's content can be retrieved using the various getXyz methods.
         * @return if information could be retrieved from input
         */
        public bool Check() {
            format = -1;
            width = -1;
            height = -1;
            bitsPerPixel = -1;
            numberOfImages = 1;
            physicalHeightDpi = -1;
            physicalWidthDpi = -1;
            comments = null;
            try {
                int b1 = read() & 0xff;
                int b2 = read() & 0xff;
                if (b1 == 0x47 && b2 == 0x49) {
                    return CheckGif();
                } else if (b1 == 0x89 && b2 == 0x50) {
                    return CheckPng();
                } else if (b1 == 0xff && b2 == 0xd8) {
                    return CheckJpeg();
                } else if (b1 == 0x42 && b2 == 0x4d) {
                    return CheckBmp();
                } else if (b1 == 0x0a && b2 < 0x06) {
                    return CheckPcx();
                } else if (b1 == 0x46 && b2 == 0x4f) {
                    return CheckIff();
                } else if (b1 == 0x59 && b2 == 0xa6) {
                    return CheckRas();
                } else if (b1 == 0x50 && b2 >= 0x31 && b2 <= 0x36) {
                    return CheckPnm(b2 - '0');
                } else if (b1 == 0x38 && b2 == 0x42) {
                    return CheckPsd();
                } else {
                    return false;
                }
            } catch (IOException) {
                return false;
            }
        }

        private bool CheckBmp() {
            byte[] a = new byte[44];
            if (read(a) != a.Length) {
                return false;
            }
            width = GetIntLittleEndian(a, 16);
            height = GetIntLittleEndian(a, 20);
            if (width < 1 || height < 1) {
                return false;
            }
            bitsPerPixel = GetShortLittleEndian(a, 26);
            if (bitsPerPixel != 1 && bitsPerPixel != 4 &&
                bitsPerPixel != 8 && bitsPerPixel != 16 &&
                bitsPerPixel != 24 && bitsPerPixel != 32) {
                return false;
            }
            int x = (int)(GetIntLittleEndian(a, 36) * 0.0254);
            if (x > 0) {
                SetPhysicalWidthDpi(x);
            }
            int y = (int)(GetIntLittleEndian(a, 40) * 0.0254);
            if (y > 0) {
                SetPhysicalHeightDpi(y);
            }
            format = FORMAT_BMP;
            return true;
        }

        private bool CheckGif() {
		    byte[] GIF_MAGIC_87A = new byte[4] {0x46, 0x38, 0x37, 0x61};
		    byte[] GIF_MAGIC_89A = new byte[4] {0x46, 0x38, 0x39, 0x61};
		    byte[] a = new byte[11]; // 4 from the GIF signature + 7 from the global header
		    if (read(a) != 11) {
			    return false;
		    }
		    if ((!Equals(a, 0, GIF_MAGIC_89A, 0, 4)) &&
			    (!Equals(a, 0, GIF_MAGIC_87A, 0, 4))) {
			    return false;
		    }
		    format = FORMAT_GIF;
		    width = GetShortLittleEndian(a, 4);
		    height = GetShortLittleEndian(a, 6);
		    int flags = a[8] & 0xff;
		    bitsPerPixel = ((flags >> 4) & 0x07) + 1;
		    //progressive = (flags & 0x02) != 0;
		    if (!determineNumberOfImages) {
			    return true;
		    }
		    // skip global color palette
		    if ((flags & 0x80) != 0) {
			    int tableSize = (1 << ((flags & 7) + 1)) * 3;
			    Skip(tableSize);
		    }
		    numberOfImages = 0;
		    int blockType;
		    do
		    {
			    blockType = read();
			    switch(blockType)
			    {
				    case(0x2c): // image separator
				    {
					    if (read(a, 0, 9) != 9) {
						    return false;
					    }
					    flags = a[8] & 0xff;
					    progressive = (flags & 0x40) != 0;
					    /*int locWidth = getShortLittleEndian(a, 4);
					    int locHeight = getShortLittleEndian(a, 6);
					    System.out.println("LOCAL: " + locWidth + " x " + locHeight);*/
					    int localBitsPerPixel = (flags & 0x07) + 1;
					    if (localBitsPerPixel > bitsPerPixel) {
						    bitsPerPixel = localBitsPerPixel;
					    }
					    if ((flags & 0x80) != 0) {
						    Skip((1 << localBitsPerPixel) * 3);
					    }
					    Skip(1); // initial code length
					    int n;
					    do
					    {
						    n = read();
						    if (n > 0) {
							    Skip(n);
						    }
						    else
						    if (n == -1) {
							    return false;
						    }
					    }
					    while (n > 0);
					    numberOfImages++;
					    break;
				    }
				    case(0x21): // extension
				    {
					    int extensionType = read();
					    if (collectComments && extensionType == 0xfe) {
						    StringBuilder sb = new StringBuilder();
						    int n;
						    do
						    {
							    n = read();
							    if (n == -1) {
								    return false;
							    }
							    if (n > 0) {
								    for (int i = 0; i < n; i++) {
									    int ch = read();
									    if (ch == -1) {
										    return false;
									    }
									    sb.Append((char)ch);
								    }
							    }
						    }
						    while (n > 0);
					    } else {
						    int n;
						    do
						    {
							    n = read();
							    if (n > 0) {
								    Skip(n);
							    }
							    else
							    if (n == -1) {
								    return false;
							    }
						    }
						    while (n > 0);
					    }
					    break;
				    }
				    case(0x3b): // end of file
				    {
					    break;
				    }
				    default:
				    {
					    return false;
				    }
			    }
		    }
		    while (blockType != 0x3b);
		    return true;
	    }

        private bool CheckIff() {
		    byte[] a = new byte[10];
		    // read remaining 2 bytes of file id, 4 bytes file size 
		    // and 4 bytes IFF subformat
		    if (read(a, 0, 10) != 10) {
			    return false;
		    }
		    byte[] IFF_RM = {0x52, 0x4d};
		    if (!Equals(a, 0, IFF_RM, 0, 2)) {
			    return false;
		    }
		    int type = GetIntBigEndian(a, 6);
		    if (type != 0x494c424d && // type must be ILBM...
		        type != 0x50424d20) { // ...or PBM
		        return false;
		    }
		    // loop chunks to find BMHD chunk
		    do {
			    if (read(a, 0, 8) != 8) {
				    return false;
			    }
			    int chunkId = GetIntBigEndian(a, 0);
			    int size = GetIntBigEndian(a, 4);
			    if ((size & 1) == 1) {
				    size++;
			    }
			    if (chunkId == 0x424d4844) { // BMHD chunk
				    if (read(a, 0, 9) != 9) {
					    return false;
				    }
				    format = FORMAT_IFF;
				    width = GetShortBigEndian(a, 0);
				    height = GetShortBigEndian(a, 2);
				    bitsPerPixel = a[8] & 0xff;
				    return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel < 33);
			    } else {
				    Skip(size);
			    }
		    } while (true);
	    }

        private bool CheckJpeg() {
		    byte[] data = new byte[12];
		    while (true) {
			    if (read(data, 0, 4) != 4) {
				    return false;
			    }
			    int marker = GetShortBigEndian(data, 0);
			    int size = GetShortBigEndian(data, 2);
			    if ((marker & 0xff00) != 0xff00) {
				    return false; // not a valid marker
			    }
			    if (marker == 0xffe0) { // APPx 
				    if (size < 14) {
					    // not an APPx header as we know it, skip
					    Skip(size - 2);
					    continue;
				    }
				    if (read(data, 0, 12) != 12) {
					    return false;
				    }
				    byte[] APP0_ID = {0x4a, 0x46, 0x49, 0x46, 0x00};
				    if (Equals(APP0_ID, 0, data, 0, 5)) {
					    //System.out.println("data 7=" + data[7]);
					    if (data[7] == 1) {
						    SetPhysicalWidthDpi(GetShortBigEndian(data, 8));
						    SetPhysicalHeightDpi(GetShortBigEndian(data, 10));
					    }
					    else
					    if (data[7] == 2) {
						    int x = GetShortBigEndian(data, 8);
						    int y = GetShortBigEndian(data, 10);
						    SetPhysicalWidthDpi((int)(x * 2.54f));
						    SetPhysicalHeightDpi((int)(y * 2.54f));
					    }
				    }
				    Skip(size - 14);
			    }
			    else
			    if (collectComments && size > 2 && marker == 0xfffe) { // comment
				    size -= 2;
				    byte[] chars = new byte[size];
				    if (read(chars, 0, size) != size) {
					    return false;
				    }

                    StringBuilder sb = new StringBuilder();
                    for (int i=0;i<chars.Length;i++) {
                        sb.Append((char)data[i]);
                    }

                    string comment = sb.ToString().Trim();
				    AddComment(comment);
			    }
			    else
			    if (marker >= 0xffc0 && marker <= 0xffcf && marker != 0xffc4 && marker != 0xffc8) {
				    if (read(data, 0, 6) != 6) {
					    return false;
				    }
				    format = FORMAT_JPEG;
				    bitsPerPixel = (data[0] & 0xff) * (data[5] & 0xff);
				    progressive = marker == 0xffc2 || marker == 0xffc6 ||
					    marker == 0xffca || marker == 0xffce;
				    width = GetShortBigEndian(data, 3);
				    height = GetShortBigEndian(data, 1);
				    return true;
			    } else {
				    Skip(size - 2);
			    }
	        }
	    }

        private bool CheckPcx() {
		    byte[] a = new byte[64];
		    if (read(a) != a.Length) {
			    return false;
		    }
		    if (a[0] != 1) { // encoding, 1=RLE is only valid value
			    return false;
		    }
		    // width / height
		    int x1 = GetShortLittleEndian(a, 2);
		    int y1 = GetShortLittleEndian(a, 4);
		    int x2 = GetShortLittleEndian(a, 6);
		    int y2 = GetShortLittleEndian(a, 8);
		    if (x1 < 0 || x2 < x1 || y1 < 0 || y2 < y1) {
			    return false;
		    }
		    width = x2 - x1 + 1;
		    height = y2 - y1 + 1;
		    // color depth
		    int bits = a[1];
		    int planes = a[63];
		    if (planes == 1 &&
		        (bits == 1 || bits == 2 || bits == 4 || bits == 8)) {
			    // paletted
			    bitsPerPixel = bits;
		    } else
		    if (planes == 3 && bits == 8) {
			    // RGB truecolor
			    bitsPerPixel = 24;
		    } else {
			    return false;
		    }
		    SetPhysicalWidthDpi(GetShortLittleEndian(a, 10));
		    SetPhysicalHeightDpi(GetShortLittleEndian(a, 10));
		    format = FORMAT_PCX;
		    return true;
	    }

        private bool CheckPng() {
		    byte[] PNG_MAGIC = {0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
		    byte[] a = new byte[27];
		    if (read(a) != 27) {
			    return false;
		    }
		    if (!Equals(a, 0, PNG_MAGIC, 0, 6)) {
			    return false;
		    }
		    format = FORMAT_PNG;
		    width = GetIntBigEndian(a, 14);
		    height = GetIntBigEndian(a, 18);
		    bitsPerPixel = a[22] & 0xff;
		    int colorType = a[23] & 0xff;
		    if (colorType == 2 || colorType == 6) {
			    bitsPerPixel *= 3;
		    }
		    progressive = (a[26] & 0xff) != 0;
		return true;
	    }

        private bool CheckPnm(int id) {
		    if (id < 1 || id > 6) {
			    return false;
		    }
		    int[] PNM_FORMATS = {FORMAT_PBM, FORMAT_PGM, FORMAT_PPM};
		    format = PNM_FORMATS[(id - 1) % 3];
		    bool hasPixelResolution = false;
		    string s;
		    while (true)
		    {
			    s = readLine();
			    if (s != null) {
                    s = s.Trim();
			    }
			    if (s == null || s.Length < 1) {
				    continue;
			    }
			    if (s[0] == '#') { // comment
				    if (collectComments && s.Length > 1) {
					    AddComment(s.Substring(1));
				    }
				    continue;
			    }
			    if (!hasPixelResolution) { // split "343 966" into width=343, height=966
                    int spaceIndex = s.IndexOf(' ');
				    if (spaceIndex == -1) {
					    return false;
				    }
				    string widthString = s.Substring(0, spaceIndex + 1);
				    spaceIndex = s.LastIndexOf(' ');
				    if (spaceIndex == -1) {
					    return false;
				    }
				    string heightString = s.Substring(spaceIndex + 1);
				    try {
                        width = Convert.ToInt32(widthString);
					    height = Convert.ToInt32(heightString);
				    } catch (Exception) {
					    return false;
				    }
				    if (width < 1 || height < 1) {
					    return false;
				    }
				    if (format == FORMAT_PBM) {
					    bitsPerPixel = 1;
					    return true;
				    }
				    hasPixelResolution = true;
			    }
			    else
			    {
				    int maxSample;
				    try {
					    maxSample = Convert.ToInt32(s);
				    } catch (Exception) {
					    return false;
				    }
				    if (maxSample < 0) {
					    return false;
				    }
				    for (int i = 0; i < 25; i++) {
					    if (maxSample < (1 << (i + 1))) {
						    bitsPerPixel = i + 1;
						    if (format == FORMAT_PPM) {
							    bitsPerPixel *= 3;
						    }
						    return true;
					    }
				    }
				    return false;
			    }
		    }
	    }

        private bool CheckPsd() {
		    byte[] a = new byte[24];
		    if (read(a) != a.Length) {
			    return false;
		    }
		    byte[] PSD_MAGIC = {0x50, 0x53};
		    if (!Equals(a, 0, PSD_MAGIC, 0, 2)) {
			    return false;
		    }
		    format = FORMAT_PSD;
		    width = GetIntBigEndian(a, 16);
		    height = GetIntBigEndian(a, 12);
		    int channels = GetShortBigEndian(a, 10);
		    int depth = GetShortBigEndian(a, 20);
		    bitsPerPixel = channels * depth;
		    return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 64);
	    }

        private bool CheckRas() {
		    byte[] a = new byte[14];
		    if (read(a) != a.Length) {
			    return false;
		    }
		    byte[] RAS_MAGIC = {0x6a, (byte)0x95};
		    if (!Equals(a, 0, RAS_MAGIC, 0, 2)) {
			    return false;
		    }
		    format = FORMAT_RAS;
		    width = GetIntBigEndian(a, 2);
		    height = GetIntBigEndian(a, 6);
		    bitsPerPixel = GetIntBigEndian(a, 10);
		    return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 24);
	    }

        private static bool Equals(byte[] a1, int offs1, byte[] a2, int offs2, int num) {
            while (num-- > 0) {
                if (a1[offs1++] != a2[offs2++]) {
                    return false;
                }
            }
            return true;
        }

        private void SetPhysicalHeightDpi(int newValue) {
            physicalHeightDpi = newValue;
        }

        private void SetPhysicalWidthDpi(int newValue) {
            physicalWidthDpi = newValue;
        }

        private static int GetIntBigEndian(byte[] a, int offs) {
            return
                (a[offs] & 0xff) << 24 |
                (a[offs + 1] & 0xff) << 16 |
                (a[offs + 2] & 0xff) << 8 |
                a[offs + 3] & 0xff;
        }

        private int GetShortLittleEndian(byte[] a, int offs) {
            return (a[offs] & 0xff) | (a[offs + 1] & 0xff) << 8;
        }

        private static int GetShortBigEndian(byte[] a, int offs) {
            return
                (a[offs] & 0xff) << 8 |
                (a[offs + 1] & 0xff);
        }

        private int GetIntLittleEndian(byte[] a, int offs) {
            return
                (a[offs + 3] & 0xff) << 24 |
                (a[offs + 2] & 0xff) << 16 |
                (a[offs + 1] & 0xff) << 8 |
                a[offs] & 0xff;
        }

        private int read(byte[] a) {
            return fs.Read(a, 0, a.Length);
        }

        private int read() {
	        return fs.ReadByte();
        }

        private int read(byte[] a, int offset, int num) {
            return fs.Read(a, offset, num);
	    }

        private string readLine() {
		    return readLine(new StringBuilder());
	    }

        private string readLine(StringBuilder sb) {
		    bool finished;
		    do {
			    int value = read();
			    finished = (value == -1 || value == 10);
			    if (!finished) {
				    sb.Append((char)value);
			    }
		    } while (!finished);
            return sb.ToString();
	    }

        private void Skip(int num) {
            while (num > 0) {
                long result;
                fs.Position = fs.Position + num;
                result = fs.Position;
                if (result > 0) {
                    num -= (int)result;
                } else {
                    result = fs.ReadByte();
                    if (result == -1) {
                	    throw new IOException("Premature end of input.");
                    } else {
                	    num--;
                    }
                }
            }
        }
	}

}

